lastModified: 2024-07-19
事情背景
今天一个盆友的公司有一个需求, 一个搜索表单, 具有展开和收起的功能, 要求操作按钮组永远在最右边. 如上图所示, 当字段数量有一行的时候, 操作按钮组在新的一行的最有边. 当字段数量不满一行的时候, 操作按钮组在未满的一行. 当字段数量超过两行的时候, 则第二行的最后一个显示为操作按钮组, 并且有展开按钮.
展开后的按钮位置同未展开样式一样.
同时要求根据分辨率不同, 控制每行显示的数量, 比如1200px展示4个, 992px展示3个, 768px展示2个, 576px展示1个...
同时展开的按钮也要根据根据当前分辨率和数量决定是否显示, 比如有六个字段时:
-
1200px时, 展开按钮不显示, 第一行四个, 第二行两个, 加一个操作按钮组
-
992px时, 展开按钮显示, 第一行三个, 第二行两个, 加一个操作按钮组
-
768px时, 展开按钮显示, 第一行两个, 第二行一个, 加一个操作按钮组
以此类推.
思路
flex
第一个想到的是flex
布局, 代码如下.
html
<div class="container"> <div class="cell">1</div> <div class="cell">2</div> <div class="cell">3</div> <div class="cell">4</div> <div class="cell">5</div> <div class="cell">6</div> <div class="cell">7</div> <div class="cell buttons">button</div> </div>
css
.container { --cols: 4; display: flex; flex-wrap: wrap; } .container .cell { min-width: 0; overflow: hidden; word-break: break-all; flex: 0 0 calc(100% / var(--cols)); height: 60px; border: solid 1px #000; box-sizing: border-box; } .container .buttons { margin-left: auto; }
效果如下:
使用flex
让.cell
元素均等分父元素, 并且flex-basis
为25%, 要求所有元素的缩放和扩张.
然后根据分辨率设置单独的flex-basis
, 比如显示三个的时候设置为33.33%, 两个的时候设置为50%.
然后让第N * 2 - 1
个.cell
后的.cell
都隐藏.
css
@media (min-width: 768px) and (max-width: 991px) { .container { --cols: 2; } .container > .cell:nth-child(3) ~ .cell:not(.buttons) { display: none; } } @media (min-width: 992px) and (max-width: 1199px) { .container { --cols: 3; } .container .cell:nth-child(5) ~ .cell:not(.buttons) { display: none; } } @media (min-width: 1200px) { .container { --cols: 4; } .container .cell:nth-child(7) ~ .cell:not(.buttons) { display: none; } }
结果如下:
正常显示, 要是展开, 只需要将隐藏的样式覆盖即可.
CSS
.container.expanded > .cell { /* 这里为了方便使用了 !important */ display: block !important; }
html
<div class="container expanded"> <div class="cell">1</div> .... <div class="cell buttons">button</div> </div>
效果如下
基于flex
布局的实现, 基本就完成了.
grid
使用flex
是能够实现, 但是这种网格式布局还是有更适合的方式, 就是grid
.
CSS
.container { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), 1fr); } .container > .cell { min-width: 0; overflow: hidden; word-break: break-all; height: 60px; border: solid 1px #000; } .container > .cell.buttons { grid-column-start: var(--cols); }
效果如下:
无需给子元素设置宽度, 父元素设置grid-template-columns
即可.
同时也不用设置box-sizing
来解决border
等影响宽度的问题.
按钮使用grid-column-start
属性固定在最后一列.
响应式部分和flex
一样.
对比
flex
布局的实现较为麻烦, 需要设置flex-basis
和box-sizing
来确保宽度,grid
布局就相对简单一点, 只需要规定列数, 使用1fr
就可以自动均分空间.grid
在较低版本的浏览器中, 无法使用gap
来设置间距, 而是使用grid-gap
.
展开和收起
如何只通过CSS
来控制展开收起按钮的显示与否呢?
思路
由于CSS
无法获取子集的数量, 因此这里只能使用JS
来设置.
同时CSS
也无法进行大于小于之类的逻辑判断, 因此实现起来略有麻烦.
经过一阵思考, 想到了使用animation
来实现, 在CSS
中唯一能够主动变化的属性?
单独设置display
在animation
中是无效的, 所以需要配合一些其他的能够过渡的属性来操作.
CSS
@keyframes show { 0% { display: block; opacity: 1; } 100% { display: none; opacity: 0; } }
然后就是如何执行这个动画, 并且应该怎么执行了.
首先需要的是知道子集的总数量, 用来判断是否该显示按钮, 这里使用vuejs
来简化部分实现:
html
<div class="container"> <div class="cell">1</div> <div class="cell">2</div> <div class="cell">3</div> <div class="cell">4</div> <div class="cell">5</div> <div class="cell">6</div> <div class="cell">7</div> <div class="cell">8</div> <div class="cell">9</div> <div class="cell buttons">button</div> </div> <div class="control">control</div>
javascript
const data = ref(...) const len = computed(() => data.value.length)
css
.control { --n: v-bind(len); --cols: 4; }
使用vuejs
的v-bind in css
的语法糖绑定了len
变量, 然后在CSS
中就可以使用--n
来获取这个变量了.
然后就是如何根据数量和分辨率控制按钮的显示和隐藏了.
animation
执行过后会返回初始状态, 所以需要添加一个animation-fill-mode: forwards;
来保持动画执行后的状态, 即保留display
的状态.
随后需要在控制动画是否执行, 使用animation-play-state
是不行的, 因为CSS
没有方式能够判断是否应该执行动画, 所以这里使用animation-iteration-count
来控制动画的执行.
当需要隐藏的时候, 就让动画的执行次数大于一次, 当需要显示的时候, 就让动画的执行次数为0.
然后通过媒体查询和数量以及前面的列数, 来设置动画的执行次数.
比如当显示4列的时候, 使用显示列数减去总数量来判断是否应该显示展开.
CSS
.control { --cols: 4; --n: v-bind(len); --run-count: min(calc((var(--cols) * 2 ) - var(--n)), 1); animation-name: show; animation-fill-mode: forwards; animation-iteration-count: var(--run-count); }
因为最多显示两行, 因此将列数*2
来计算出显示出了多少个元素.
如果使用显示列数减去总数量后, 小于0则代表总数量大于显示的数量, 需要显示展开按钮, 等于0则代表总数量等于显示的数量, 不需要显示按钮.
同时因为动画执行次数无法为负数, 所以小于0也就不会执行动画了, 因为被视为了无效设置, 默认就会显示按钮, 同时等于0也不会执行.
为什么不是列数*2-1
排除掉末尾的按钮呢, 因为显示列数大于总数量的时候是不需要显示按钮的, 因此在相同的时候需要让值为正数, 让动画执行一次.
例如一行4个总数7个的时候, (4*2)-7=1
而 (4*2-1)-7=0
会导致动画没有执行, 不执行就不会隐藏, 但是这里是需要隐藏的.
而数量为8的时候, 因为相差为0, 所以不会执行动画, 所以会显示按钮.再大的总数会成为负数, 导致动画失效.
为了防止当显示数量大于总数量多个的时候, 动画的多次执行, 所以使用min
函数限制执行最大次数为1.
CSS
@media screen and (min-width: 481px) and (max-width: 767px) { .control { --cols: 2; } } @media screen and (min-width: 769px) { .control { --cols: 3; } } @media screen and (min-width: 1200px) { .control { --cols: 4; } }
由此就完成了基本全由CSS
控制的按钮显隐, 只需要JS
传递总数量, 其他的行为均为CSS
完成.
总结
之后还有不通过JS
来控制展开行为之类的, 由于这部分内容就比较简单了, 所以就不在这里多做赘述.
总之这是一次有趣的CSS
应用经验, 如果不认真去想的话, 大概第一反应都是使用JS
来实现吧.
不过虽然CSS
能够实现, 但是也确实相对来说比较麻烦, 需要一些技巧来实现.
同时需要对CSS
的能力有一定的认知, 现在的CSS
可以说是排除兼容性问题后, 已经非常强大了(虽然和本文无关).
最后附上完整的grid
实现代码:
html
<div class="container"> <div class="cell">1</div> <div class="cell">2</div> <div class="cell">3</div> <div class="cell">4</div> <div class="cell">5</div> <div class="cell">6</div> <div class="cell">7</div> <div class="cell buttons">button</div> </div> <div class="control">control</div>
javascript
const data = ref(...) const len = computed(() => data.value.length)
css
.container { --cols: 4; display: grid; grid-template-columns: repeat(var(--cols), 1fr); } .container > .cell { min-width: 0; overflow: hidden; word-break: break-all; height: 60px; border: solid 1px #000; } .container > .cell.buttons { grid-column-start: var(--cols); } .control { --cols: 4; --n: v-bind(len); --run-count: min(calc((var(--cols) * 2) - var(--n)), 1); animation-name: show; animation-fill-mode: forwards; animation-iteration-count: var(--run-count); } @media (min-width: 768px) and (max-width: 991px) { .container { --cols: 2; } .control { --cols: 2; } .container > .cell:nth-child(3) ~ .cell:not(.buttons) { display: none; } } @media (min-width: 992px) and (max-width: 1199px) { .control { --cols: 3; } .container { --cols: 3; } .container .cell:nth-child(5) ~ .cell:not(.buttons) { display: none; } } @media (min-width: 1200px) { .container { --cols: 4; } .control { --cols: 4; } .container .cell:nth-child(7) ~ .cell:not(.buttons) { display: none; } } @keyframes show { 0% { display: block; opacity: 1; } 100% { display: none; opacity: 0; } }
AI结语
本文不仅是一次对CSS极限能力的探索,也是对传统布局与交互实现方式的一次挑战。虽然过程中不得不轻微触碰JavaScript来辅助,但核心逻辑与控制依旧牢牢掌握在CSS手中,证明了在某些场景下,CSS也能成为实现复杂界面逻辑的得力工具。对于前端开发者而言,这无疑是一次启发思维、拓宽视野的宝贵经验分享。